package com.sleepbot.datetimepicker.time;
/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorProperty;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.Component;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.app.Context;
import ohos.global.resource.ResourceManager;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;

import com.fourmob.datetimepicker.ResourceTable;

/**
 * View to show what number is selected. This will draw a blue circle over the number, with a blue
 * line coming from the center of the main circle to the edge of the blue selection.
 */
public class RadialSelectorView extends Component implements Component.DrawTask {
    private static final String TAG = "RadialSelectorView";
    private static HiLogLabel label = new HiLogLabel(HiLog.LOG_APP, 0x000110, "DateTimePickerRSV");
    private final Paint mPaint = new Paint();

    private boolean mIsInitialized;
    private boolean mDrawValuesReady;

    private float mCircleRadiusMultiplier;
    private float mAmPmCircleRadiusMultiplier;
    private float mInnerNumbersRadiusMultiplier;
    private float mOuterNumbersRadiusMultiplier;
    private float mNumbersRadiusMultiplier;
    private float mSelectionRadiusMultiplier;
    private float mAnimationRadiusMultiplier;
    private boolean mIs24HourMode;
    private boolean mHasInnerCircle;

    private int mXCenter;
    private int mYCenter;
    private int mCircleRadius;
    private int mLineLength;
    private int mSelectionRadius;

    private int mSelectionDegrees;
    private double mSelectionRadians;
    private boolean mForceDrawDot;

    private int blueColor,norblue;

    public RadialSelectorView(Context context) {
        super(context);
        mIsInitialized = false;
    }

    /**
     * Initialize this selector with the state of the picker.
     *
     * @param context          Current context.
     * @param is24HourMode     Whether the selector is in 24-hour mode, which will tell us
     *                         whether the circle's center is moved up slightly to make room for the AM/PM circles.
     * @param hasInnerCircle   Whether we have both an inner and an outer circle of numbers
     *                         that may be selected. Should be true for 24-hour mode in the hours circle.
     * @param disappearsOut    Whether the numbers' animation will have them disappearing out
     *                         or disappearing in.
     * @param selectionDegrees The initial degrees to be selected.
     * @param isInnerCircle    Whether the initial selection is in the inner or outer circle.
     *                         Will be ignored when hasInnerCircle is false.
     */
    public void initialize(Context context, boolean is24HourMode, boolean hasInnerCircle,
                           boolean disappearsOut, int selectionDegrees, boolean isInnerCircle) {
        addDrawTask(this::onDraw);
        if (mIsInitialized) {
            HiLog.error(label, "This RadialSelectorView may only be initialized once.");
            return;
        }
        try {
            ResourceManager resourceManager = context.getResourceManager();

            int blue = resourceManager.getElement(ResourceTable.Color_light_blue).getColor();
            blueColor = resourceManager.getElement(ResourceTable.Color_blue).getColor();
            norblue = blue;
            mPaint.setColor(new Color(blue));
            mPaint.setAntiAlias(true);

            // Calculate values for the circle radius size.
            mIs24HourMode = is24HourMode;
            if (is24HourMode) {
                mCircleRadiusMultiplier = Float.parseFloat(
                        resourceManager.getElement(ResourceTable.
                            String_circle_radius_multiplier_24HourMode).getString());
            } else {
                mCircleRadiusMultiplier = Float.parseFloat(
                        resourceManager.getElement(ResourceTable
                            .String_circle_radius_multiplier).getString());
                mAmPmCircleRadiusMultiplier = Float.parseFloat(
                        resourceManager.getElement(ResourceTable
                            .String_ampm_circle_radius_multiplier).getString());
            }

            mHasInnerCircle = hasInnerCircle;
            if (hasInnerCircle) {
                mInnerNumbersRadiusMultiplier =
                        Float.parseFloat(resourceManager.getElement(ResourceTable
                            .String_numbers_radius_multiplier_inner).getString());
                mOuterNumbersRadiusMultiplier =
                        Float.parseFloat(resourceManager.getElement(ResourceTable
                            .String_numbers_radius_multiplier_outer).getString());
            } else {
                mNumbersRadiusMultiplier =
                        Float.parseFloat(resourceManager.getElement(ResourceTable
                            .String_numbers_radius_multiplier_normal).getString());
            }
            mSelectionRadiusMultiplier =
                    Float.parseFloat(resourceManager.getElement(ResourceTable
                        .String_selection_radius_multiplier).getString());
            mAnimationRadiusMultiplier = 1;

            setSelection(selectionDegrees, isInnerCircle, false);
            mIsInitialized = true;
        } catch (Exception e) {
            e.printStackTrace();
        }

        mAnimationRadiusMultiplier = 1;

        setSelection(selectionDegrees, isInnerCircle, false);
        mIsInitialized = true;
    }

    /**
     * Set the selection.
     *
     * @param selectionDegrees The degrees to be selected.
     * @param isInnerCircle    Whether the selection should be in the inner circle or outer. Will be
     *                         ignored if hasInnerCircle was initialized to false.
     * @param forceDrawDot     Whether to force the dot in the center of the selection circle to be
     *                         drawn. If false, the dot will be drawn only when
     *                         the degrees is not a multiple of 30, i.e.
     *                         the selection is not on a visible number.
     */
    public void setSelection(int selectionDegrees, boolean isInnerCircle, boolean forceDrawDot) {
        mSelectionDegrees = selectionDegrees;
        mSelectionRadians = selectionDegrees * Math.PI / 180;
        mForceDrawDot = forceDrawDot;

        if (mHasInnerCircle) {
            if (isInnerCircle) {
                mNumbersRadiusMultiplier = mInnerNumbersRadiusMultiplier;
            } else {
                mNumbersRadiusMultiplier = mOuterNumbersRadiusMultiplier;
            }
        }
    }

    public int getDegreesFromCoords(float pointX, float pointY, boolean forceLegal,
                                    final Boolean[] isInnerCircle) {
        if (!mDrawValuesReady) {
            return -1;
        }
        double hypotenuse = Math.sqrt((pointY - mYCenter) * (pointY - mYCenter)
                    + (pointX - mXCenter) * (pointX - mXCenter));

        // Check if we're outside the range
        if (mHasInnerCircle) {
            if (forceLegal) {
                // If we're told to force the coordinates to be legal, we'll set the isInnerCircle
                // boolean based based off whichever number the coordinates are closer to.
                int innerNumberRadius = (int) (mCircleRadius * mInnerNumbersRadiusMultiplier);
                int distanceToInnerNumber = (int) Math.abs(hypotenuse - innerNumberRadius);
                int outerNumberRadius = (int) (mCircleRadius * mOuterNumbersRadiusMultiplier);
                int distanceToOuterNumber = (int) Math.abs(hypotenuse - outerNumberRadius);
                isInnerCircle[0] = (distanceToInnerNumber <= distanceToOuterNumber);
            } else {
                // Otherwise, if we're close enough to either number (with the space between the
                // two allotted equally), set the isInnerCircle boolean as the closer one.
                // appropriately, but otherwise return -1.
                int minAllowedHypotenuseForInnerNumber =
                        (int) (mCircleRadius * mInnerNumbersRadiusMultiplier) - mSelectionRadius;
                int maxAllowedHypotenuseForOuterNumber =
                        (int) (mCircleRadius * mOuterNumbersRadiusMultiplier) + mSelectionRadius;
                int halfwayHypotenusePoint = (int) (mCircleRadius
                    * ((mOuterNumbersRadiusMultiplier + mInnerNumbersRadiusMultiplier) / 2));

                if (hypotenuse >= minAllowedHypotenuseForInnerNumber
                    && hypotenuse <= halfwayHypotenusePoint) {
                    isInnerCircle[0] = true;
                } else if (hypotenuse <= maxAllowedHypotenuseForOuterNumber
                    && hypotenuse >= halfwayHypotenusePoint) {
                    isInnerCircle[0] = false;
                } else {
                    return -1;
                }
            }
        } else {
            // If there's just one circle, we'll need to return -1 if:
            // we're not told to force the coordinates to be legal, and
            // the coordinates' distance to the number is within the allowed distance.
            if (!forceLegal) {
                int distanceToNumber = (int) Math.abs(hypotenuse - mLineLength);

                // The max allowed distance will be defined as the distance from the center of the
                // number to the edge of the circle.
                int maxAllowedDistance = (int) (mCircleRadius * (1 - mNumbersRadiusMultiplier));
                if (distanceToNumber > maxAllowedDistance) {
                    return -1;
                }
            }
        }

        float opposite = Math.abs(pointY - mYCenter);
        double radians = Math.asin(opposite / hypotenuse);
        int degrees = (int) (radians * 180 / Math.PI);

        // Now we have to translate to the correct quadrant.
        boolean rightSide = (pointX > mXCenter);
        boolean topSide = (pointY < mYCenter);
        if (rightSide && topSide) {
            degrees = 90 - degrees;
        } else if (rightSide && !topSide) {
            degrees = 90 + degrees;
        } else if (!rightSide && !topSide) {
            degrees = 270 - degrees;
        } else if (!rightSide && topSide) {
            degrees = 270 + degrees;
        }
        return degrees;
    }

    public AnimatorProperty getDisappearAnimator(Component component) {
        if (!mIsInitialized || !mDrawValuesReady) {
            HiLog.error(label, "RadialSelectorView was not ready for animation.");
            return null;
        }

        AnimatorProperty disappearAnimator = new AnimatorProperty();
        disappearAnimator.setTarget(component);
        disappearAnimator.setStateChangedListener(new Animator.StateChangedListener() {
            @Override
            public void onStart(Animator animator) {
            }

            @Override
            public void onStop(Animator animator) {
            }

            @Override
            public void onCancel(Animator animator) {
            }

            @Override
            public void onEnd(Animator animator) {
                component.setAlpha(0);
            }

            @Override
            public void onPause(Animator animator) {
            }

            @Override
            public void onResume(Animator animator) {
            }
        });

        return disappearAnimator;
    }

    public AnimatorProperty getReappearAnimator(Component v) {
        v.setVisibility(VISIBLE);
        if (!mIsInitialized || !mDrawValuesReady) {
            HiLog.error(label, "RadialSelectorView was not ready for animation.");
            return null;
        }

        // The time points are half of what they would normally be, because this animation is
        // staggered against the disappear so they happen seamlessly. The reappear starts
        // halfway into the disappear.
        AnimatorProperty animatorValue = new AnimatorProperty();
        animatorValue.setStateChangedListener(new Animator.StateChangedListener() {
            @Override
            public void onStart(Animator animator) {
            }

            @Override
            public void onStop(Animator animator) {
            }

            @Override
            public void onCancel(Animator animator) {
            }

            @Override
            public void onEnd(Animator animator) {
                   animatorValue.reset();
                   v.setAlpha(255);
            }

            @Override
            public void onPause(Animator animator) {
            }

            @Override
            public void onResume(Animator animator) {
            }
        });
        animatorValue.setTarget(v);
        return animatorValue;
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        HiLog.info(label,"onDraw");
        int viewWidth = getWidth();
        if (viewWidth == 0 || !mIsInitialized) {
            return;
        }
        HiLog.info(label,"onDraw01");
        if (!mDrawValuesReady) {
            mXCenter = getWidth() / 2;
            mYCenter = getHeight() / 2;
            mCircleRadius = (int) (Math.min(mXCenter, mYCenter) * mCircleRadiusMultiplier);
            HiLog.info(label,"onDraw1a");
            if (!mIs24HourMode) {
                // We'll need to draw the AM/PM circles, so the main circle will need to have
                // a slightly higher center. To keep the entire view centered vertically, we'll
                // have to push it up by half the radius of the AM/PM circles.
                int amPmCircleRadius = (int) (mCircleRadius * mAmPmCircleRadiusMultiplier);
                mYCenter -= amPmCircleRadius / 2;
            }

            mSelectionRadius = (int) (mCircleRadius * mSelectionRadiusMultiplier);
            mDrawValuesReady = true;
        }
        HiLog.info(label,"onDraw02");
        mLineLength = (int) (mCircleRadius * mNumbersRadiusMultiplier * mAnimationRadiusMultiplier);
        int pointX = mXCenter + (int) (mLineLength * Math.sin(mSelectionRadians));
        int pointY = mYCenter - (int) (mLineLength * Math.cos(mSelectionRadians));
        HiLog.info(label,"onDraw03");

        HiLog.info(label,"onDraw04");

        mPaint.setColor(new Color(norblue));
        canvas.drawCircle(pointX, pointY, mSelectionRadius, mPaint);
        HiLog.info(label,"onDraw05");
        if (mForceDrawDot || mSelectionDegrees % 30 != 0) {
            // We're not on a direct tick (or we've been told to draw the dot anyway).
            mPaint.setColor(new Color(blueColor));
            canvas.drawCircle(pointX, pointY, mSelectionRadius * 2 / (float)7, mPaint);
        } else {
            // We're not drawing the dot, so shorten the line to only go as far as the edge of the
            // selection circle.
            int lineLength = mLineLength;
            lineLength -= mSelectionRadius;
            pointX = mXCenter + (int) (lineLength * Math.sin(mSelectionRadians));
            pointY = mYCenter - (int) (lineLength * Math.cos(mSelectionRadians));
        }

        // Draw the line from the center of the circle.
        mPaint.setStrokeWidth(2);

        Point start = new Point(mXCenter,mYCenter);
        Point end = new Point(pointX,pointY);
        mPaint.setColor(new Color(norblue));
        canvas.drawLine(start,end,mPaint);
    }

    /**
     * We'll need to invalidate during the animation.
     */
    private static class InvalidateUpdateListener implements AnimatorValue.ValueUpdateListener {
        @Override
        public void onUpdate(AnimatorValue animatorValue, float v) {
        }
    }
}